package org.hepeng.workx.mybatis.session;

import com.google.common.collect.Sets;
import org.apache.ibatis.builder.xml.XMLConfigBuilder;
import org.apache.ibatis.datasource.DataSourceFactory;
import org.apache.ibatis.exceptions.ExceptionFactory;
import org.apache.ibatis.executor.ErrorContext;
import org.apache.ibatis.mapping.Environment;
import org.apache.ibatis.parsing.XNode;
import org.apache.ibatis.parsing.XPathParser;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.apache.ibatis.transaction.TransactionFactory;
import org.hepeng.workx.jdbc.DataSourceMetadataStore;
import org.hepeng.workx.jdbc.SelectableDataSource;
import org.hepeng.workx.mybatis.builder.xml.XMLConfigBuilderX;
import org.hepeng.workx.jdbc.DataSourceMetadata;
import org.hepeng.workx.mybatis.event.listener.ExecuteEventListener;
import org.hepeng.workx.mybatis.util.WorkXMybatisEnvironment;
import org.joor.Reflect;

import javax.sql.DataSource;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Properties;
import java.util.Set;

import static org.hepeng.workx.mybatis.MybatisConstant.*;

/**
 * @author he peng
 */
public class SqlSessionFactoryBuilderX extends SqlSessionFactoryBuilder {

    protected Set<ExecuteEventListener> executeEventListeners = Sets.newConcurrentHashSet();
    private XMLConfigBuilder xmlConfigBuilder;

    public SqlSessionFactoryBuilderX executeEventListeners(Set<ExecuteEventListener> executeEventListeners) {
        this.executeEventListeners = executeEventListeners;
        return this;
    }

    public XMLConfigBuilder getXmlConfigBuilder() {
        return xmlConfigBuilder;
    }

    public void setXmlConfigBuilder(XMLConfigBuilder xmlConfigBuilder) {
        this.xmlConfigBuilder = xmlConfigBuilder;
    }

    public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
        return build(new InputStreamReader(inputStream) , environment , properties);
    }

    public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
        try {
            this.xmlConfigBuilder = new XMLConfigBuilderX(reader, environment, properties );
            Configuration configuration = this.xmlConfigBuilder.parse();
            return build(configuration);
        } catch (Exception e) {
            throw ExceptionFactory.wrapException("Error building SqlSession.", e);
        } finally {
            ErrorContext.instance().reset();
            try {
                reader.close();
            } catch (IOException e) {
                // Intentionally ignore. Prefer previous error.
            }
        }
    }

    @Override
    public SqlSessionFactory build(Configuration configuration) {
        if (ConfigurationX.class.isAssignableFrom(configuration.getClass())) {
            ConfigurationX configurationX = (ConfigurationX) configuration;
            configurationX.addExecuteEventListener(this.executeEventListeners);
        }
        Properties variables = configuration.getVariables();
        if (WorkXMybatisEnvironment.isEnableDataSourceRoute(variables)) {
            parseEnvironmentsElement(this.xmlConfigBuilder);
        }
        return super.build(configuration);
    }

    private void parseEnvironmentsElement(XMLConfigBuilder parser) {
        Reflect parserReflect = Reflect.on(parser);
        XPathParser xPathParser = parserReflect.get("parser");
        XNode root = xPathParser.evalNode("/configuration");
        XNode environments = root.evalNode("environments");
        if (Objects.isNull(environments)) {
            return;
        }

        Configuration configuration = parserReflect.get("configuration");

        try {
            SelectableDataSource selectableDataSource = null;
            List<DataSourceMetadata> dataSourceMetadataList = new ArrayList<>();
            for (XNode child : environments.getChildren()) {
                String id = child.getStringAttribute("id");
                XNode dataSourceNode = child.evalNode("dataSource");
                if (Objects.nonNull(dataSourceNode)) {
                    TransactionFactory txFactory = parserReflect.call("transactionManagerElement" , child.evalNode("transactionManager")).get();
                    String type = dataSourceNode.getStringAttribute("type");
                    Properties props = dataSourceNode.getChildrenAsProperties();
                    Class<?> resolveClass = parserReflect.call("resolveClass", type).get();
                    DataSourceFactory factory = (DataSourceFactory) resolveClass.newInstance();
                    DataSourceMetadata.DataSourceOpsMode dataSourceMode = WorkXMybatisEnvironment.getDataSourceMode(props);
                    props.remove(DATASOURCE_OPS_MODE);
                    factory.setProperties(props);

                    DataSource dataSource = factory.getDataSource();
                    if (SelectableDataSource.class.isAssignableFrom(dataSource.getClass())) {
                        selectableDataSource = (SelectableDataSource) dataSource;
                        Environment.Builder environmentBuilder = new Environment.Builder(id)
                                .transactionFactory(txFactory)
                                .dataSource(dataSource);
                        configuration.setEnvironment(environmentBuilder.build());
                    } else {
                        DataSourceMetadata dataSourceMetaData = new DataSourceMetadata(id , dataSource , dataSourceMode);
                        dataSourceMetadataList.add(dataSourceMetaData);
                    }
                }
            }

            if (Objects.nonNull(selectableDataSource)) {
                DataSourceMetadataStore.storeDataSourceMetaData(dataSourceMetadataList);
            }
        } catch (Exception e) {
            throw ExceptionFactory.wrapException("" , e);
        }
    }

}
